<?php

namespace JLPay\Services;

use JLPay\Core\OrgConfig;
use JLPay\Core\Http\HttpRequest;
use JLPay\Core\Http\DefaultHttpClientBuilder;
use JLPay\Core\Http\HttpClient;
use JLPay\Core\SignVerifier;
use JLPay\Models\BaseResponse;
use JLPay\Utils\Logger;
use JLPay\Utils\SmUtils;
use JLPay\Exceptions\HttpExecutionException;
use JLPay\Exceptions\SignVerifyException;
use JLPay\Exceptions\ConfigException;
use JLPay\Exceptions\CryptoException;
use Exception;

/**
 * 服务接口基类
 */
abstract class BaseService
{
    protected OrgConfig $config;
    protected HttpClient $httpClient;
    protected SignVerifier $signVerifier;
    protected Logger $logger;

    /**
     * 初始化服务接口
     *
     * @param OrgConfig $config 配置对象
     */
    public function __construct(OrgConfig $config)
    {
        $this->config = $config;
        $this->httpClient = DefaultHttpClientBuilder::build();
        $this->signVerifier = new SignVerifier($config);
        $this->logger = Logger::getInstance();
    }

    /**
     * 获取API URL
     *
     * @param string $requestClass 请求类名
     * @param string|null $apiPath 接口路径
     * @return string 完整的API URL
     */
    abstract public function getApiUrl(string $requestClass, ?string $apiPath = null): string;

    /**
     * 发送POST请求
     *
     * @param object $request 请求对象
     * @param string $responseClass 响应类名
     * @param string|null $apiPath 接口路径
     * @return object 响应对象
     * @throws ConfigException
     * @throws HttpExecutionException
     * @throws SignVerifyException
     */
    public function post(object $request, string $responseClass, ?string $apiPath = null): object
    {
        return $this->execute($request, $responseClass, 'POST', $apiPath);
    }

    /**
     * 发送GET请求
     *
     * @param object $request 请求对象
     * @param string $responseClass 响应类名
     * @param string|null $apiPath 接口路径
     * @return object 响应对象
     * @throws ConfigException
     * @throws HttpExecutionException
     * @throws SignVerifyException
     */
    public function get(object $request, string $responseClass, ?string $apiPath = null): object
    {
        return $this->execute($request, $responseClass, 'GET', $apiPath);
    }

    /**
     * 执行HTTP请求
     *
     * @param object $request 请求对象
     * @param string $responseClass 响应类名
     * @param string $method HTTP方法
     * @param string|null $apiPath 接口路径
     * @return object 响应对象
     * @throws ConfigException
     * @throws HttpExecutionException
     * @throws SignVerifyException
     */
    protected function execute(object $request, string $responseClass, string $method, ?string $apiPath = null): object
    {
        // 校验请求参数
        if (method_exists($request, 'checkRequiredParams')) {
            $request->checkRequiredParams($this->config);
        }

        // 构建完整URL
        $url = $this->getApiUrl(get_class($request), $apiPath);

        // 获取URI
        $uri = $this->extractUri($url);

        // 处理请求加密
        $cryptoHeaders = [];
        try {
            $this->handleEncrypt($request, $cryptoHeaders);
        } catch (Exception $e) {
            $this->logger->error("处理请求加密时发生错误: " . $e->getMessage(), ['exception' => $e]);
            throw new CryptoException("处理请求加密失败: " . $e->getMessage(), 0, $e);
        }

        // 将请求对象转换为数组
        $body = $this->requestToArray($request);

        // 签名
        [$signValue, $timestamp, $nonce] = $this->signVerifier->sign($method, $uri, $body);

        // 构建HTTP请求
        $httpRequest = HttpRequest::builder()
            ->url($url)
            ->method($method)
            ->header('Content-Type', 'application/json; charset=utf-8')
            ->header('x-jlpay-appid', $this->config->getAppId())
            ->header('x-jlpay-nonce', $nonce)
            ->header('x-jlpay-timestamp', $timestamp)
            ->header('x-jlpay-sign-alg', 'SM3WithSM2WithDer')
            ->header('x-jlpay-sign', $signValue);

        // 添加必需的请求头
        // 从请求数据中提取商户号
        $merchNo = $body['merch_no'] ?? $body['mch_id'] ?? null;
        if ($merchNo) {
            $httpRequest->header('x-jlpay-merchno', $merchNo);
        }
        
        // 从请求数据中提取机构号
        $orgNo = $body['org_no'] ?? null;
        if ($orgNo) {
            $httpRequest->header('x-jlpay-orgno', $orgNo);
        }

        // 添加加密相关的请求头
        $this->logger->debug("加密相关的请求头", $cryptoHeaders);
        foreach ($cryptoHeaders as $key => $value) {
            $httpRequest->header($key, $value);
        }

        $httpRequest->body($body);
        $httpRequestObj = $httpRequest->build();

        // 打印HTTP请求头和内容
        $this->logger->debug("=== HTTP请求信息 ===");
        $this->logger->debug("请求URL: " . $url);
        $this->logger->debug("请求方法: " . $method);
        $this->logger->debug("请求头:", $httpRequestObj->getHeaders());
        $this->logger->debug("请求体: " . json_encode($body, JSON_UNESCAPED_UNICODE));
        $this->logger->debug("==================");

        // 执行请求
        $response = $this->httpClient->execute($httpRequestObj);

        // 调试HTTP响应
        $this->logger->debug("=== HTTP响应信息 ===");
        $this->logger->debug("响应状态码: " . $response->getStatus());
        $this->logger->debug("响应头:", $response->getHeaders());
        $this->logger->debug("响应体: " . $response->getBody());
        $this->logger->debug("==================");

        // 验证响应签名
        if (isset($response->getHeaders()['x-jlpay-sign'])) {
            $this->signVerifier->verify(
                $method,
                $uri,
                $response->getHeaders()['x-jlpay-timestamp'] ?? '',
                $response->getHeaders()['x-jlpay-nonce'] ?? '',
                $response->getBody(),
                $response->getHeaders()['x-jlpay-sign']
            );
        }

        // 解析响应
        try {
            $responseData = json_decode($response->getBody(), true);
            if ($responseData === null) {
                throw new HttpExecutionException("解析响应JSON失败: " . json_last_error_msg());
            }

            // 创建响应对象
            $responseObj = $responseClass === BaseResponse::class
                ? new BaseResponse($responseData)
                : new $responseClass($responseData);

            // 处理响应解密
            try {
                $this->handleDecrypt($responseObj, $response->getHeaders());
            } catch (Exception $e) {
                $this->logger->error("处理响应解密时发生错误: " . $e->getMessage(), ['exception' => $e]);
                // 仅记录错误，不影响正常流程
            }

            return $responseObj;
        } catch (Exception $e) {
            $this->logger->error("解析响应JSON失败: " . $e->getMessage() . ", 原始响应: " . substr($response->getBody(), 0, 200) . "...", ['exception' => $e]);
            throw new HttpExecutionException("解析响应JSON失败: " . $e->getMessage(), 0, $e);
        }
    }

    /**
     * 从URL中提取URI部分
     *
     * @param string $url
     * @return string
     */
    private function extractUri(string $url): string
    {
        if (strpos($url, '.com') !== false) {
            return explode('.com', $url)[1];
        }

        $parsed = parse_url($url);
        return ($parsed['path'] ?? '') . (isset($parsed['query']) ? '?' . $parsed['query'] : '');
    }

    /**
     * 将请求对象转换为数组
     *
     * @param object $request
     * @return array
     */
    protected function requestToArray(object $request): array
    {
        if (method_exists($request, 'toArray')) {
            return $request->toArray();
        }

        // 反射获取对象属性
        $reflection = new \ReflectionClass($request);
        $properties = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED | \ReflectionProperty::IS_PRIVATE);

        $result = [];
        foreach ($properties as $property) {
            $property->setAccessible(true);
            $value = $property->getValue($request);
            if ($value !== null) {
                $result[$property->getName()] = $value;
            }
        }

        return $result;
    }

    /**
     * 处理请求加密
     *
     * @param object $request 请求对象
     * @param array $headers 请求头数组，用于添加加密相关的头信息
     * @throws CryptoException
     */
    protected function handleEncrypt(object $request, array &$headers): void
    {
        // 打印加密相关配置信息，用于诊断
        $autoEncrypt = $this->config->isAutoEncrypt();
        $cryptoAlg = $this->config->getCryptoAlg();
        $this->logger->debug("加密配置信息", [
            'auto_encrypt' => $autoEncrypt,
            'crypto_alg' => $cryptoAlg
        ]);

        // 检查是否需要加密
        if (!$this->requiresEncrypt($request)) {
            $this->logger->debug("请求类未标记为需要加密，跳过加密处理");
            return;
        }

        // 获取敏感字段
        $sensitiveFields = $this->getSensitiveFields($request);
        if (empty($sensitiveFields)) {
            $this->logger->debug("未发现需要加密的敏感字段，跳过加密处理");
            return;
        } else {
            $this->logger->debug("发现需要加密的敏感字段", $sensitiveFields);
        }

        // 检查是否启用自动加密
        if (!$autoEncrypt) {
            $this->logger->warning("警告: 发现敏感字段但自动加密未启用！建议启用auto_encrypt=true");
            $headers["x-jlpay-crypto-alg"] = "SM2WithSM4";
            return;
        }

        // 检查是否支持SM2WithSM4加密
        if ($cryptoAlg !== "SM2WithSM4") {
            $this->logger->warning("警告: 不支持的加密算法: {$cryptoAlg}，建议设置crypto_alg='SM2WithSM4'");
            $headers["x-jlpay-crypto-alg"] = "SM2WithSM4";
            return;
        }

        try {
            // 生成SM4密钥
            $sm4Key = SmUtils::generateSm4Key();

            // 将请求对象转换为数组
            $requestArray = $this->requestToArray($request);

            // 加密敏感字段
            foreach ($sensitiveFields as $field) {
                $fieldPath = is_string($field) ? $field : $field['field_path'] ?? '';
                
                // 处理嵌套字段路径
                $this->encryptNestedField($requestArray, $request, $fieldPath, $sm4Key);
            }

            // 使用SM2加密SM4密钥
            try {
                // SmUtils::sm2Encrypt 直接返回 base64 编码的密文，不需要额外转换
                $encryptedKey = SmUtils::sm2Encrypt($sm4Key, $this->config->getJlpayPubKeyHex());

                // 将加密后的密钥添加到请求头
                $headers["x-jlpay-crypto-alg"] = "SM2WithSM4";
                $headers["x-jlpay-key"] = $encryptedKey;

                // 打印关键信息以确认头部设置成功
                $this->logger->debug("加密密钥已添加到请求头: x-jlpay-key=" . substr($encryptedKey, 0, 10) . "...");
            } catch (Exception $e) {
                $this->logger->error("加密SM4密钥失败: " . $e->getMessage(), ['exception' => $e]);
                throw new CryptoException("加密SM4密钥失败: " . $e->getMessage(), 0, $e);
            }
        } catch (Exception $e) {
            $this->logger->error("请求加密过程发生错误: " . $e->getMessage(), ['exception' => $e]);
            throw new CryptoException("请求加密失败: " . $e->getMessage(), 0, $e);
        }
    }

    /**
     * 加密嵌套字段
     *
     * @param array $requestArray 请求数据数组
     * @param object $request 请求对象
     * @param string $fieldPath 字段路径，如 'baseInfo.contact_info.contact_name'
     * @param string $sm4Key SM4密钥
     * @throws CryptoException
     */
    protected function encryptNestedField(array $requestArray, object $request, string $fieldPath, string $sm4Key): void
    {
        $pathParts = explode('.', $fieldPath);
        $value = $this->getNestedValue($requestArray, $pathParts);
        
        if ($value === null) {
            $this->logger->debug("字段路径 {$fieldPath} 的值为空，跳过加密");
            return;
        }

        $this->logger->debug("正在加密字段 {$fieldPath}，原始值: " . (is_string($value) ? substr($value, 0, 20) . "..." : json_encode($value)));

        try {
            // 将字段值转换为JSON字符串（如果是复杂对象）或保持原值（如果是简单类型）
            if (is_array($value) || is_object($value)) {
                $value = json_encode($value, JSON_UNESCAPED_UNICODE);
            }

            // 使用SM4加密
            $encryptedValue = SmUtils::sm4EcbEncrypt($value, $sm4Key);
            
            // 设置加密后的值到请求对象中
            $this->setNestedObjectProperty($request, $pathParts, $encryptedValue);
            
            $this->logger->debug("字段 {$fieldPath} 加密成功，加密值: " . substr($encryptedValue, 0, 20) . "...");
        } catch (Exception $e) {
            $this->logger->error("加密字段 {$fieldPath} 失败: " . $e->getMessage(), ['exception' => $e]);
            throw new CryptoException("加密字段 {$fieldPath} 失败: " . $e->getMessage(), 0, $e);
        }
    }

    /**
     * 获取嵌套值
     *
     * @param array $data 数据数组
     * @param array $pathParts 路径部分数组
     * @return mixed
     */
    protected function getNestedValue(array $data, array $pathParts)
    {
        $current = $data;
        
        foreach ($pathParts as $part) {
            if (!is_array($current) || !isset($current[$part])) {
                return null;
            }
            $current = $current[$part];
        }
        
        return $current;
    }

    /**
     * 设置嵌套对象属性值
     *
     * @param object $object 对象
     * @param array $pathParts 路径部分数组
     * @param mixed $value 要设置的值
     */
    protected function setNestedObjectProperty(object $object, array $pathParts, $value): void
    {
        if (count($pathParts) === 1) {
            // 直接属性 - 需要转换下划线格式到驼峰格式
            $camelCaseProperty = $this->snakeToCamel($pathParts[0]);
            $this->setObjectProperty($object, $camelCaseProperty, $value);
            return;
        }

        // 嵌套属性 - 转换顶级属性名为驼峰格式
        $reflection = new \ReflectionClass($object);
        $topProperty = $this->snakeToCamel($pathParts[0]);
        
        if (!$reflection->hasProperty($topProperty)) {
            $this->logger->warning("对象中不存在属性: {$topProperty} (原路径: {$pathParts[0]})");
            return;
        }

        $prop = $reflection->getProperty($topProperty);
        $prop->setAccessible(true);
        $topValue = $prop->getValue($object);
        
        if (!is_array($topValue)) {
            $this->logger->warning("属性 {$topProperty} 不是数组，无法设置嵌套值");
            return;
        }

        // 修改嵌套数组值 - 保持下划线格式用于数组键
        $this->setNestedArrayValue($topValue, array_slice($pathParts, 1), $value);
        
        // 设置回对象
        $prop->setValue($object, $topValue);
        
        $this->logger->debug("成功设置嵌套属性 {$topProperty}[" . implode('.', array_slice($pathParts, 1)) . "] = " . (is_string($value) ? substr($value, 0, 20) . "..." : json_encode($value)));
    }

    /**
     * 将下划线命名转换为驼峰命名
     *
     * @param string $snakeCase
     * @return string
     */
    protected function snakeToCamel(string $snakeCase): string
    {
        return lcfirst(str_replace('_', '', ucwords($snakeCase, '_')));
    }

    /**
     * 设置嵌套数组值
     *
     * @param array &$array 数组引用
     * @param array $pathParts 路径部分数组
     * @param mixed $value 要设置的值
     */
    protected function setNestedArrayValue(array &$array, array $pathParts, $value): void
    {
        if (count($pathParts) === 1) {
            $array[$pathParts[0]] = $value;
            return;
        }

        $key = $pathParts[0];
        if (!isset($array[$key]) || !is_array($array[$key])) {
            $array[$key] = [];
        }
        
        $this->setNestedArrayValue($array[$key], array_slice($pathParts, 1), $value);
    }

    /**
     * 处理响应解密
     *
     * @param object $response 响应对象
     * @param array $headers 响应头数组
     */
    protected function handleDecrypt(object $response, array $headers): void
    {
        // 检查是否启用自动解密
        if (!$this->config->isAutoDecrypt()) {
            $this->logger->debug("自动解密未启用，跳过解密处理");
            return;
        }

        // 检查是否支持SM2+SM4解密
        $cryptoAlg = $headers['x-jlpay-crypto-alg'] ?? '';
        if ($cryptoAlg !== "SM2WithSM4") {
            $this->logger->debug("不支持的加密算法: {$cryptoAlg}，跳过解密处理");
            return;
        }

        // 检查是否需要解密
        if (!$this->requiresDecrypt($response)) {
            // 兼容旧版实现，成功响应都可能需要解密
            if (method_exists($response, 'isSuccess') && !$response->isSuccess()) {
                $this->logger->debug("响应不成功，跳过解密处理");
                return;
            }
        } else {
            // 如果明确标记了需要解密，但响应不成功，也不进行解密
            if (method_exists($response, 'isSuccess') && !$response->isSuccess()) {
                $this->logger->debug("响应不成功，跳过解密处理");
                return;
            }
        }

        // 获取加密的密钥
        $encryptedKey = $headers['x-jlpay-key'] ?? '';
        if (empty($encryptedKey)) {
            $this->logger->debug("未找到加密密钥，跳过解密处理");
            return;
        }

        try {
            // SmUtils::sm2Decrypt 返回base64编码的SM4密钥，需要解码为原始字节
            $sm4KeyBase64 = SmUtils::sm2Decrypt($encryptedKey, $this->config->getOrgPriKeyHex(), $this->config->getJlpayPubKeyHex());
            $sm4KeyBytes = base64_decode($sm4KeyBase64);
            
            if ($sm4KeyBytes === false) {
                $this->logger->error("SM4密钥base64解码失败");
                return;
            }
            
            // 获取原始响应数据
            $responseData = method_exists($response, 'getOriginData') ? $response->getOriginData() : null;
            if (!$responseData) {
                return;
            }

            // 获取需要解密的敏感字段列表
            $sensitiveFields = $this->getSensitiveFields($response);
            $sensitiveFieldNames = [];
            foreach ($sensitiveFields as $field) {
                if (is_string($field)) {
                    $sensitiveFieldNames[] = $field;
                } elseif (is_object($field) && method_exists($field, 'getFieldPath')) {
                    $sensitiveFieldNames[] = $field->getFieldPath();
                }
            }

            // 只对敏感字段进行解密
            foreach ($responseData as $field => $value) {
                // 只处理敏感字段
                if (in_array($field, $sensitiveFieldNames) && is_string($value) && trim($value) !== '') {
                    // 检查是否是Base64编码的字符串（可能是加密数据）
                    try {
                        // 尝试Base64解码
                        $decodedBytes = base64_decode($value, true);
                        // 如果解码成功且长度合理，尝试解密
                        if ($decodedBytes !== false && strlen($decodedBytes) > 0) {
                            try {
                                // 使用原始字节密钥进行解密
                                $decryptedValue = SmUtils::sm4EcbDecrypt($value, base64_encode($sm4KeyBytes));
                                
                                // 尝试解析JSON
                                $jsonValue = json_decode($decryptedValue, true);
                                if ($jsonValue !== null) {
                                    $this->setObjectProperty($response, $field, $jsonValue);
                                } else {
                                    $this->setObjectProperty($response, $field, $decryptedValue);
                                }
                                $this->logger->debug("成功解密字段 {$field}");
                            } catch (Exception $e) {
                                $this->logger->error("解密字段 {$field} 失败: " . $e->getMessage(), ['exception' => $e]);
                            }
                        }
                    } catch (Exception $e) {
                        // Base64解码失败，说明不是加密数据，保持原值
                        // 不记录错误，因为这是正常情况
                    }
                }
            }
        } catch (Exception $e) {
            $this->logger->error("响应解密过程发生错误: " . $e->getMessage(), ['exception' => $e]);
            // 不抛出异常，避免影响正常业务流程
            // 解密失败不影响业务，只是保持原值
        }
    }

    /**
     * 检查是否需要加密
     *
     * @param object $request
     * @return bool
     */
    protected function requiresEncrypt(object $request): bool
    {
        // 可以通过注解或接口来检查
        return method_exists($request, 'requiresEncrypt') ? $request->requiresEncrypt() : false;
    }

    /**
     * 检查是否需要解密
     *
     * @param object $response
     * @return bool
     */
    protected function requiresDecrypt(object $response): bool
    {
        // 可以通过注解或接口来检查
        return method_exists($response, 'requiresDecrypt') ? $response->requiresDecrypt() : false;
    }

    /**
     * 获取敏感字段列表
     *
     * @param object $object 请求或响应对象
     * @return array
     */
    protected function getSensitiveFields(object $object): array
    {
        // 优先使用对象自己的getSensitiveFields方法
        if (method_exists($object, 'getSensitiveFields')) {
            return $object->getSensitiveFields();
        }
        
        // 对于响应对象，如果没有明确的敏感字段定义，返回空数组
        // 这样可以避免对所有字段进行解密尝试
        return [];
    }

    /**
     * 设置对象属性值
     *
     * @param object $object
     * @param string $property
     * @param mixed $value
     */
    protected function setObjectProperty(object $object, string $property, $value): void
    {
        // 将下划线格式转换为驼峰格式
        $camelCaseProperty = $this->snakeToCamel($property);
        
        if (property_exists($object, $camelCaseProperty)) {
            $reflection = new \ReflectionClass($object);
            $prop = $reflection->getProperty($camelCaseProperty);
            $prop->setAccessible(true);
            $prop->setValue($object, $value);
            $this->logger->debug("成功设置属性 {$camelCaseProperty} = " . (is_string($value) ? substr($value, 0, 20) . "..." : json_encode($value)));
        } else {
            $this->logger->warning("对象中不存在属性: {$camelCaseProperty} (原字段: {$property})");
        }
    }
} 